/*!
 * qiniu-js-sdk v@VERSION
 *
 * Copyright 2015 by Qiniu
 * Released under GPL V2 License.
 *
 * GitHub: http://github.com/qiniu/js-sdk
 *
 * Date: @DATE
*/

/*global plupload ,mOxie*/
/*global ActiveXObject */
/*exported Qiniu */
/*exported QiniuJsSDK */

;(function( global ){

    /**
     * Creates new cookie or removes cookie with negative expiration
     * @param  key       The key or identifier for the store
     * @param  value     Contents of the store
     * @param  exp       Expiration - creation defaults to 30 days
     */
    function createCookie(key, value, exp) {
        var date = new Date();
        date.setTime(date.getTime() + (exp * 24 * 60 * 60 * 1000));
        var expires = "; expires=" + date.toGMTString();
        document.cookie = key + "=" + value + expires + "; path=/";
    }
    
    /**
     * Returns contents of cookie
     * @param  key       The key or identifier for the store
     */
    function readCookie(key) {
        var nameEQ = key + "=";
        var ca = document.cookie.split(';');
        for (var i = 0, max = ca.length; i < max; i++) {
            var c = ca[i];
            while (c.charAt(0) === ' ') {
                c = c.substring(1, c.length);
            }
            if (c.indexOf(nameEQ) === 0) {
                return c.substring(nameEQ.length, c.length);
            }
        }
        return null;
    }
    
    // if current browser is not support localStorage
    // use cookie to make a polyfill
    if ( !window.localStorage ) {
        window.localStorage = {
            setItem: function (key, value) {
                createCookie(key, value, 30);
            },
            getItem: function (key) {
                return readCookie(key);
            },
            removeItem: function (key) {
                createCookie(key, '', -1);
            }
        };
    }
    
    function QiniuJsSDK() {
    
        var that = this;
    
        /**
         * detect IE version
         * if current browser is not IE
         *     it will return false
         * else
         *     it will return version of current IE browser
         * @return {Number|Boolean} IE version or false
         */
        this.detectIEVersion = function() {
            var v = 4,
                div = document.createElement('div'),
                all = div.getElementsByTagName('i');
            while (
                div.innerHTML = '<!--[if gt IE ' + v + ']><i></i><![endif]-->',
                all[0]
            ) {
                v++;
            }
            return v > 4 ? v : false;
        };
    
        var logger = {
            MUTE: 0,
            FATA: 1,
            ERROR: 2,
            WARN: 3,
            INFO: 4,
            DEBUG: 5,
            TRACE: 6,
            level: 0
        };
    
        function log(type, args){
            var header = "[qiniu-js-sdk]["+type+"]";
            var msg = header;
            for (var i = 0; i < args.length; i++) {
                if (typeof args[i] === "string") {
                    msg += " " + args[i];
                } else {
                    msg += " " + that.stringifyJSON(args[i]);
                }
            }
            if (that.detectIEVersion()) {
                // http://stackoverflow.com/questions/5538972/console-log-apply-not-working-in-ie9
                //var log = Function.prototype.bind.call(console.log, console);
                //log.apply(console, args);
                console.log(msg);
            }else{
                args.unshift(header);
                console.log.apply(console, args);
            }
            if (document.getElementById('qiniu-js-sdk-log')) {
                document.getElementById('qiniu-js-sdk-log').innerHTML += '<p>'+msg+'</p>';
            }
        }
    
        function makeLogFunc(code){
            var func = code.toLowerCase();
            logger[func] = function(){
                // logger[func].history = logger[func].history || [];
                // logger[func].history.push(arguments);
                if(window.console && window.console.log && logger.level>=logger[code]){
                    var args = Array.prototype.slice.call(arguments);
                    log(func,args);
                }
            };
        }
    
        for (var property in logger){
            if (logger.hasOwnProperty(property) && (typeof logger[property]) === "number" && !logger.hasOwnProperty(property.toLowerCase())) {
                makeLogFunc(property);
            }
        }
    
    
        var qiniuUploadUrl;
        if (window.location.protocol === 'https:') {
            qiniuUploadUrl = 'https://up.qbox.me';
        } else {
            qiniuUploadUrl = 'http://upload.qiniu.com';
        }
    
        /**
         * qiniu upload urls
         * 'qiniuUploadUrls' is used to change target when current url is not avaliable
         * @type {Array}
         */
        var qiniuUploadUrls = [
            // "https://up.qbox.me"
            "http://upload.qiniu.com",
            "http://up.qiniu.com",
        ];
    
        var changeUrlTimes = 0;
    
        /**
         * reset upload url
         * if current page protocal is https
         *     it will always return 'https://up.qbox.me'
         * else
         *     it will set 'qiniuUploadUrl' value with 'qiniuUploadUrls' looply
         */
        this.resetUploadUrl = function(){
            if (window.location.protocol === 'https:') {
                qiniuUploadUrl = 'https://up.qbox.me';
            } else {
                var i = changeUrlTimes % qiniuUploadUrls.length;
                qiniuUploadUrl = qiniuUploadUrls[i];
                changeUrlTimes++;
            }
            logger.debug('resetUploadUrl: '+qiniuUploadUrl);
        };
    
        this.resetUploadUrl();
    
    
        /**
         * is image
         * @param  {String}  url of a file
         * @return {Boolean} file is a image or not
         */
        this.isImage = function(url) {
            url = url.split(/[?#]/)[0];
            return (/\.(png|jpg|jpeg|gif|bmp)$/i).test(url);
        };
    
        /**
         * get file extension
         * @param  {String} filename
         * @return {String} file extension
         * @example
         *     input: test.txt
         *     output: txt
         */
        this.getFileExtension = function(filename) {
            var tempArr = filename.split(".");
            var ext;
            if (tempArr.length === 1 || (tempArr[0] === "" && tempArr.length === 2)) {
                ext = "";
            } else {
                ext = tempArr.pop().toLowerCase(); //get the extension and make it lower-case
            }
            return ext;
        };
    
        /**
         * encode string by utf8
         * @param  {String} string to encode
         * @return {String} encoded string
         */
        this.utf8_encode = function(argString) {
            // http://kevin.vanzonneveld.net
            // +   original by: Webtoolkit.info (http://www.webtoolkit.info/)
            // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
            // +   improved by: sowberry
            // +    tweaked by: Jack
            // +   bugfixed by: Onno Marsman
            // +   improved by: Yves Sucaet
            // +   bugfixed by: Onno Marsman
            // +   bugfixed by: Ulrich
            // +   bugfixed by: Rafal Kukawski
            // +   improved by: kirilloid
            // +   bugfixed by: kirilloid
            // *     example 1: this.utf8_encode('Kevin van Zonneveld');
            // *     returns 1: 'Kevin van Zonneveld'
    
            if (argString === null || typeof argString === 'undefined') {
                return '';
            }
    
            var string = (argString + ''); // .replace(/\r\n/g, '\n').replace(/\r/g, '\n');
            var utftext = '',
                start, end, stringl = 0;
    
            start = end = 0;
            stringl = string.length;
            for (var n = 0; n < stringl; n++) {
                var c1 = string.charCodeAt(n);
                var enc = null;
    
                if (c1 < 128) {
                    end++;
                } else if (c1 > 127 && c1 < 2048) {
                    enc = String.fromCharCode(
                        (c1 >> 6) | 192, (c1 & 63) | 128
                    );
                } else if (c1 & 0xF800 ^ 0xD800 > 0) {
                    enc = String.fromCharCode(
                        (c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
                    );
                } else { // surrogate pairs
                    if (c1 & 0xFC00 ^ 0xD800 > 0) {
                        throw new RangeError('Unmatched trail surrogate at ' + n);
                    }
                    var c2 = string.charCodeAt(++n);
                    if (c2 & 0xFC00 ^ 0xDC00 > 0) {
                        throw new RangeError('Unmatched lead surrogate at ' + (n - 1));
                    }
                    c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
                    enc = String.fromCharCode(
                        (c1 >> 18) | 240, ((c1 >> 12) & 63) | 128, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128
                    );
                }
                if (enc !== null) {
                    if (end > start) {
                        utftext += string.slice(start, end);
                    }
                    utftext += enc;
                    start = end = n + 1;
                }
            }
    
            if (end > start) {
                utftext += string.slice(start, stringl);
            }
    
            return utftext;
        };
    
        /**
         * encode data by base64
         * @param  {String} data to encode
         * @return {String} encoded data
         */
        this.base64_encode = function(data) {
            // http://kevin.vanzonneveld.net
            // +   original by: Tyler Akins (http://rumkin.com)
            // +   improved by: Bayron Guevara
            // +   improved by: Thunder.m
            // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
            // +   bugfixed by: Pellentesque Malesuada
            // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
            // -    depends on: this.utf8_encode
            // *     example 1: this.base64_encode('Kevin van Zonneveld');
            // *     returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
            // mozilla has this native
            // - but breaks in 2.0.0.12!
            //if (typeof this.window['atob'] == 'function') {
            //    return atob(data);
            //}
            var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
            var o1, o2, o3, h1, h2, h3, h4, bits, i = 0,
                ac = 0,
                enc = '',
                tmp_arr = [];
    
            if (!data) {
                return data;
            }
    
            data = this.utf8_encode(data + '');
    
            do { // pack three octets into four hexets
                o1 = data.charCodeAt(i++);
                o2 = data.charCodeAt(i++);
                o3 = data.charCodeAt(i++);
    
                bits = o1 << 16 | o2 << 8 | o3;
    
                h1 = bits >> 18 & 0x3f;
                h2 = bits >> 12 & 0x3f;
                h3 = bits >> 6 & 0x3f;
                h4 = bits & 0x3f;
    
                // use hexets to index into b64, and append result to encoded string
                tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
            } while (i < data.length);
    
            enc = tmp_arr.join('');
    
            switch (data.length % 3) {
                case 1:
                    enc = enc.slice(0, -2) + '==';
                    break;
                case 2:
                    enc = enc.slice(0, -1) + '=';
                    break;
            }
    
            return enc;
        };
    
        /**
         * encode string in url by base64
         * @param {String} string in url
         * @return {String} encoded string
         */
        this.URLSafeBase64Encode = function(v) {
            v = this.base64_encode(v);
            return v.replace(/\//g, '_').replace(/\+/g, '-');
        };
    
        // TODO: use mOxie
        /**
         * craete object used to AJAX
         * @return {Object}
         */
        this.createAjax = function(argument) {
            var xmlhttp = {};
            if (window.XMLHttpRequest) {
                xmlhttp = new XMLHttpRequest();
            } else {
                xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
            }
            return xmlhttp;
        };
    
        // TODO: enhance IE compatibility
        /**
         * parse json string to javascript object
         * @param  {String} json string
         * @return {Object} object
         */
        this.parseJSON = function(data) {
            // Attempt to parse using the native JSON parser first
            if (window.JSON && window.JSON.parse) {
                return window.JSON.parse(data);
            }
    
            //var rx_one = /^[\],:{}\s]*$/,
            //    rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
            //    rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
            //    rx_four = /(?:^|:|,)(?:\s*\[)+/g,
            var    rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
    
            //var json;
    
            var text = String(data);
            rx_dangerous.lastIndex = 0;
            if(rx_dangerous.test(text)){
                text = text.replace(rx_dangerous, function(a){
                   return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                });
            }
    
            // todo 使用一下判断,增加安全性
            //if (
            //    rx_one.test(
            //        text
            //            .replace(rx_two, '@')
            //            .replace(rx_three, ']')
            //            .replace(rx_four, '')
            //    )
            //) {
            //    return eval('(' + text + ')');
            //}
    
            return eval('('+text+')');
        };
    
        /**
         * parse javascript object to json string
         * @param  {Object} object
         * @return {String} json string
         */
        this.stringifyJSON = function(obj) {
            // Attempt to parse using the native JSON parser first
            if (window.JSON && window.JSON.stringify) {
                return window.JSON.stringify(obj);
            }
            switch (typeof (obj)) {
                case 'string':
                    return '"' + obj.replace(/(["\\])/g, '\\$1') + '"';
                case 'array':
                    return '[' + obj.map(that.stringifyJSON).join(',') + ']';
                case 'object':
                    if (obj instanceof Array) {
                        var strArr = [];
                        var len = obj.length;
                        for (var i = 0; i < len; i++) {
                            strArr.push(that.stringifyJSON(obj[i]));
                        }
                        return '[' + strArr.join(',') + ']';
                    } else if (obj === null) {
                        return 'null';
                    } else {
                        var string = [];
                        for (var property in obj) {
                            if (obj.hasOwnProperty(property)) {
                                string.push(that.stringifyJSON(property) + ':' + that.stringifyJSON(obj[property]));
                            }
                        }
                        return '{' + string.join(',') + '}';
                    }
                    break;
                case 'number':
                    return obj;
                case false:
                    return obj;
                case 'boolean':
                    return obj;
            }
        };
    
        /**
         * trim space beside text
         * @param  {String} untrimed string
         * @return {String} trimed string
         */
        this.trim = function(text) {
            return text === null ? "" : text.replace(/^\s+|\s+$/g, '');
        };
    
        /**
         * create a uploader by QiniuJsSDK
         * @param  {object} options to create a new uploader
         * @return {object} uploader
         */
        this.uploader = function(op) {
    
            /********** inner function define start **********/
    
            // according the different condition to reset chunk size
            // and the upload strategy according with the chunk size
            // when chunk size is zero will cause to direct upload
            // see the statement binded on 'BeforeUpload' event
            var reset_chunk_size = function() {
                var ie = that.detectIEVersion();
                var BLOCK_BITS, MAX_CHUNK_SIZE, chunk_size;
                // case Safari 5、Windows 7、iOS 7 set isSpecialSafari to true
                var isSpecialSafari = (mOxie.Env.browser === "Safari" && mOxie.Env.version <= 5 && mOxie.Env.os === "Windows" && mOxie.Env.osVersion === "7") || (mOxie.Env.browser === "Safari" && mOxie.Env.os === "iOS" && mOxie.Env.osVersion === "7");
                // case IE 9-，chunk_size is not empty and flash is included in runtimes
                // set op.chunk_size to zero
                //if (ie && ie <= 9 && op.chunk_size && op.runtimes.indexOf('flash') >= 0) {
                if (ie && ie <= 9 && op.chunk_size && op.runtimes.indexOf('flash') < 0) {
                    //  link: http://www.plupload.com/docs/Frequently-Asked-Questions#when-to-use-chunking-and-when-not
                    //  when plupload chunk_size setting is't null ,it cause bug in ie8/9  which runs  flash runtimes (not support html5) .
                    op.chunk_size = 0;
                } else if (isSpecialSafari) {
                    // win7 safari / iOS7 safari have bug when in chunk upload mode
                    // reset chunk_size to 0
                    // disable chunk in special version safari
                    op.chunk_size = 0;
                } else {
                    BLOCK_BITS = 20;
                    MAX_CHUNK_SIZE = 4 << BLOCK_BITS; //4M
    
                    chunk_size = plupload.parseSize(op.chunk_size);
                    if (chunk_size > MAX_CHUNK_SIZE) {
                        op.chunk_size = MAX_CHUNK_SIZE;
                    }
                    // qiniu service  max_chunk_size is 4m
                    // reset chunk_size to max_chunk_size(4m) when chunk_size > 4m
                }
                // if op.chunk_size set 0 will be cause to direct upload
            };
    
            // getUptoken maybe called at Init Event or BeforeUpload Event
            // case Init Event, the file param of getUptken will be set a null value
            // if op.uptoken has value, set uptoken with op.uptoken
            // else if op.uptoken_url has value, set uptoken from op.uptoken_url
            // else if op.uptoken_func has value, set uptoken by result of op.uptoken_func
            var getUpToken = function(file) {
                if (op.uptoken) {
                    that.token = op.uptoken;
                    return;
                } else if (op.uptoken_url) {
                    logger.debug("get uptoken from: ", that.uptoken_url);
                    // TODO: use mOxie
                    var ajax = that.createAjax();
                    ajax.open('GET', that.uptoken_url, false);
                    ajax.setRequestHeader("If-Modified-Since", "0");
                    // 开启跨域带cookie
                    ajax.withCredentials = true;
                    // ajax.onreadystatechange = function() {
                    //     if (ajax.readyState === 4 && ajax.status === 200) {
                    //         var res = that.parseJSON(ajax.responseText);
                    //         that.token = res.uptoken;
                    //     }
                    // };
                    ajax.send();
                    if (ajax.status === 200) {
                        var res = that.parseJSON(ajax.responseText);
                        that.token = res.uptoken;
                        logger.debug("get new uptoken: ", res.uptoken);
                    } else {
                        logger.error("get uptoken error: ", ajax.responseText);
                    }
                    return;
                } else if (op.uptoken_func) {
                    logger.debug("get uptoken from uptoken_func");
                    that.token = op.uptoken_func(file);
                    logger.debug("get new uptoken: ", that.token);
                    return;
                } else {
                    logger.error("one of [uptoken, uptoken_url, uptoken_func] settings in options is required!");
                }
            };
    
            // get file key according with the user passed options
            var getFileKey = function(up, file, func) {
                // TODO: save_key can read from scope of token
                var key = '',
                    unique_names = false;
                if (!op.save_key) {
                    unique_names = up.getOption && up.getOption('unique_names');
                    unique_names = unique_names || (up.settings && up.settings.unique_names);
                    if (unique_names) {
                        var ext = that.getFileExtension(file.name);
                        key = ext ? file.id + '.' + ext : file.id;
                    } else if (typeof func === 'function') {
                        key = func(up, file);
                    } else {
                        key = file.name;
                    }
                }
                return key;
            };
    
            /********** inner function define end **********/
    
            if (op.log_level) {
                logger.level = op.log_level;
            }
    
            if (!op.domain) {
                throw 'domain setting in options is required!';
            }
    
            if (!op.browse_button) {
                throw 'browse_button setting in options is required!';
            }
    
            if (!op.uptoken && !op.uptoken_url && !op.uptoken_func) {
                throw 'one of [uptoken, uptoken_url, uptoken_func] settings in options is required!';
            }
    
            logger.debug("init uploader start");
    
            logger.debug("environment: ", mOxie.Env);
    
            logger.debug("userAgent: ", navigator.userAgent);
    
            var option = {};
    
            // hold the handler from user passed options
            var _Error_Handler = op.init && op.init.Error;
            var _FileUploaded_Handler = op.init && op.init.FileUploaded;
    
            // replace the handler for intercept
            op.init.Error = function() {};
            op.init.FileUploaded = function() {};
    
            that.uptoken_url = op.uptoken_url;
            that.token = '';
            that.key_handler = typeof op.init.Key === 'function' ? op.init.Key : '';
            this.domain = op.domain;
            // TODO: ctx is global in scope of a uploader instance
            // this maybe cause error
            var ctx = '';
            var speedCalInfo = {
                isResumeUpload: false,
                resumeFilesize: 0,
                startTime: '',
                currentTime: ''
            };
    
            reset_chunk_size();
            logger.debug("invoke reset_chunk_size()");
            logger.debug("op.chunk_size: ", op.chunk_size);
    
            // compose options with user passed options and default setting
            plupload.extend(option, op, {
                url: qiniuUploadUrl,
                multipart_params: {
                    token: ''
                }
            });
    
            logger.debug("option: ", option);
    
            // create a new uploader with composed options
            var uploader = new plupload.Uploader(option);
    
            logger.debug("new plupload.Uploader(option)");
    
            // bind getUpToken to 'Init' event
            uploader.bind('Init', function(up, params) {
                logger.debug("Init event activated");
                // if op.get_new_uptoken is not true
                //      invoke getUptoken when uploader init
                // else
                //      getUptoken everytime before a new file upload
                if(!op.get_new_uptoken){
                    getUpToken(null);
                }
                //getUpToken(null);
            });
    
            logger.debug("bind Init event");
    
            // bind 'FilesAdded' event
            // when file be added and auto_start has set value
            // uploader will auto start upload the file
            uploader.bind('FilesAdded', function(up, files) {
                logger.debug("FilesAdded event activated");
                var auto_start = up.getOption && up.getOption('auto_start');
                auto_start = auto_start || (up.settings && up.settings.auto_start);
                logger.debug("auto_start: ", auto_start);
                logger.debug("files: ", files);
    
                // detect is iOS
                var is_ios = function (){
                    if(mOxie.Env.OS.toLowerCase()==="ios") {
                        return true;
                    } else {
                        return false;
                    }
                };
    
                // if current env os is iOS change file name to [time].[ext]
                if (is_ios()) {
                    for (var i = 0; i < files.length; i++) {
                        var file = files[i];
                        var ext = that.getFileExtension(file.name);
                        file.name = file.id + "." + ext;
                    }
                }
    
                if (auto_start) {
                    setTimeout(function(){
                        up.start();
                        logger.debug("invoke up.start()");
                    }, 0);
                    // up.start();
                    // plupload.each(files, function(i, file) {
                    //     up.start();
                    //     logger.debug("invoke up.start()")
                    //     logger.debug("file: ", file);
                    // });
                }
                up.refresh(); // Reposition Flash/Silverlight
            });
    
            logger.debug("bind FilesAdded event");
    
            // bind 'BeforeUpload' event
            // intercept the process of upload
            // - prepare uptoken
            // - according the chunk size to make differnt upload strategy
            // - resume upload with the last breakpoint of file
            uploader.bind('BeforeUpload', function(up, file) {
                logger.debug("BeforeUpload event activated");
                // add a key named speed for file object
                file.speed = file.speed || 0;
                ctx = '';
    
                if(op.get_new_uptoken){
                    getUpToken(file);
                }
    
                var directUpload = function(up, file, func) {
                    speedCalInfo.startTime = new Date().getTime();
                    var multipart_params_obj;
                    if (op.save_key) {
                        multipart_params_obj = {
                            'token': that.token
                        };
                    } else {
                        multipart_params_obj = {
                            'key': getFileKey(up, file, func),
                            'token': that.token
                        };
                    }
    
                    logger.debug("directUpload multipart_params_obj: ", multipart_params_obj);
    
                    var x_vars = op.x_vars;
                    if (x_vars !== undefined && typeof x_vars === 'object') {
                        for (var x_key in x_vars) {
                            if (x_vars.hasOwnProperty(x_key)) {
                                if (typeof x_vars[x_key] === 'function') {
                                    multipart_params_obj['x:' + x_key] = x_vars[x_key](up, file);
                                } else if (typeof x_vars[x_key] !== 'object') {
                                    multipart_params_obj['x:' + x_key] = x_vars[x_key];
                                }
                            }
                        }
                    }
    
    
                    up.setOption({
                        'url': qiniuUploadUrl,
                        'multipart': true,
                        'chunk_size': is_android_weixin_or_qq() ? op.max_file_size : undefined,
                        'multipart_params': multipart_params_obj
                    });
                };
    
                // detect is weixin or qq inner browser
                var is_android_weixin_or_qq = function (){
                    var ua = navigator.userAgent.toLowerCase();
                    if((ua.match(/MicroMessenger/i) || mOxie.Env.browser === "QQBrowser" || ua.match(/V1_AND_SQ/i)) && mOxie.Env.OS.toLowerCase()==="android") {
                        return true;
                    } else {
                        return false;
                    }
                };
    
                var chunk_size = up.getOption && up.getOption('chunk_size');
                chunk_size = chunk_size || (up.settings && up.settings.chunk_size);
    
                logger.debug("uploader.runtime: ",uploader.runtime);
                logger.debug("chunk_size: ",chunk_size);
    
                // TODO: flash support chunk upload
                if ((uploader.runtime === 'html5' || uploader.runtime === 'flash') && chunk_size) {
                    if (file.size < chunk_size || is_android_weixin_or_qq()) {
                        logger.debug("directUpload because file.size < chunk_size || is_android_weixin_or_qq()");
                        // direct upload if file size is less then the chunk size
                        directUpload(up, file, that.key_handler);
                    } else {
                        // TODO: need a polifill to make it work in IE 9-
                        // ISSUE: if file.name is existed in localStorage
                        // but not the same file maybe cause error
                        var localFileInfo = localStorage.getItem(file.name);
                        var blockSize = chunk_size;
                        if (localFileInfo) {
                            // TODO: although only the html5 runtime will enter this statement
                            // but need uniform way to make convertion between string and json
                            localFileInfo = that.parseJSON(localFileInfo);
                            var now = (new Date()).getTime();
                            var before = localFileInfo.time || 0;
                            var aDay = 24 * 60 * 60 * 1000; //  milliseconds of one day
                            // if the last upload time is within one day
                            //      will upload continuously follow the last breakpoint
                            // else
                            //      will reupload entire file
                            if (now - before < aDay) {
    
                                if (localFileInfo.percent !== 100) {
                                    if (file.size === localFileInfo.total) {
                                        // TODO: if file.name and file.size is the same
                                        // but not the same file will cause error
                                        file.percent = localFileInfo.percent;
                                        file.loaded = localFileInfo.offset;
                                        ctx = localFileInfo.ctx;
    
                                        // set speed info
                                        speedCalInfo.isResumeUpload = true;
                                        speedCalInfo.resumeFilesize = localFileInfo.offset;
    
                                        // set block size
                                        if (localFileInfo.offset + blockSize > file.size) {
                                            blockSize = file.size - localFileInfo.offset;
                                        }
                                    } else {
                                        // remove file info when file.size is conflict with file info
                                        localStorage.removeItem(file.name);
                                    }
    
                                } else {
                                    // remove file info when upload percent is 100%
                                    // avoid 499 bug
                                    localStorage.removeItem(file.name);
                                }
                            } else {
                                // remove file info when last upload time is over one day
                                localStorage.removeItem(file.name);
                            }
                        }
                        speedCalInfo.startTime = new Date().getTime();
                        // TODO: to support bput
                        // http://developer.qiniu.com/docs/v6/api/reference/up/bput.html
                        up.setOption({
                            'url': qiniuUploadUrl + '/mkblk/' + blockSize,
                            'multipart': false,
                            'chunk_size': chunk_size,
                            'required_features': "chunks",
                            'headers': {
                                'Authorization': 'UpToken ' + that.token
                            },
                            'multipart_params': {}
                        });
                    }
                } else {
                    logger.debug("directUpload because uploader.runtime !== 'html5' || uploader.runtime !== 'flash' || !chunk_size");
                    // direct upload if runtime is not html5
                    directUpload(up, file, that.key_handler);
                }
            });
    
            logger.debug("bind BeforeUpload event");
    
            // bind 'UploadProgress' event
            // calculate upload speed
            uploader.bind('UploadProgress', function(up, file) {
                logger.trace("UploadProgress event activated");
                speedCalInfo.currentTime = new Date().getTime();
                var timeUsed = speedCalInfo.currentTime - speedCalInfo.startTime; // ms
                var fileUploaded = file.loaded || 0;
                if (speedCalInfo.isResumeUpload) {
                    fileUploaded = file.loaded - speedCalInfo.resumeFilesize;
                }
                file.speed = (fileUploaded / timeUsed * 1000).toFixed(0) || 0; // unit: byte/s
            });
    
            logger.debug("bind UploadProgress event");
    
            // bind 'ChunkUploaded' event
            // store the chunk upload info and set next chunk upload url
            uploader.bind('ChunkUploaded', function(up, file, info) {
                logger.debug("ChunkUploaded event activated");
                logger.debug("file: ", file);
                logger.debug("info: ", info);
                var res = that.parseJSON(info.response);
                logger.debug("res: ", res);
                // ctx should look like '[chunk01_ctx],[chunk02_ctx],[chunk03_ctx],...'
                ctx = ctx ? ctx + ',' + res.ctx : res.ctx;
                var leftSize = info.total - info.offset;
                var chunk_size = up.getOption && up.getOption('chunk_size');
                chunk_size = chunk_size || (up.settings && up.settings.chunk_size);
                if (leftSize < chunk_size) {
                    up.setOption({
                        'url': qiniuUploadUrl + '/mkblk/' + leftSize
                    });
                    logger.debug("up.setOption url: ", qiniuUploadUrl + '/mkblk/' + leftSize);
                }
                localStorage.setItem(file.name, that.stringifyJSON({
                    ctx: ctx,
                    percent: file.percent,
                    total: info.total,
                    offset: info.offset,
                    time: (new Date()).getTime()
                }));
            });
    
            logger.debug("bind ChunkUploaded event");
    
            var retries = qiniuUploadUrls.length;
    
            // if error is unkown switch upload url and retry
            var unknow_error_retry = function(file){
                if (retries-- > 0) {
                    setTimeout(function(){
                        that.resetUploadUrl();
                        file.status = plupload.QUEUED;
                        uploader.stop();
                        uploader.start();
                    }, 0);
                    return true;
                }else{
                    retries = qiniuUploadUrls.length;
                    return false;
                }
            };
    
            // bind 'Error' event
            // check the err.code and return the errTip
            uploader.bind('Error', (function(_Error_Handler) {
                return function(up, err) {
                    logger.error("Error event activated");
                    logger.error("err: ", err);
                    var errTip = '';
                    var file = err.file;
                    if (file) {
                        switch (err.code) {
                            case plupload.FAILED:
                                errTip = '上传失败。请稍后再试。';
                                break;
                            case plupload.FILE_SIZE_ERROR:
                                var max_file_size = up.getOption && up.getOption('max_file_size');
                                max_file_size = max_file_size || (up.settings && up.settings.max_file_size);
                                errTip = '浏览器最大可上传' + max_file_size + '。更大文件请使用命令行工具。';
                                break;
                            case plupload.FILE_EXTENSION_ERROR:
                                errTip = '文件验证失败。请稍后重试。';
                                break;
                            case plupload.HTTP_ERROR:
                                if (err.response === '') {
                                    // Fix parseJSON error ,when http error is like net::ERR_ADDRESS_UNREACHABLE
                                    errTip = err.message || '未知网络错误。';
                                    if (!unknow_error_retry(file)) {
                                        return;
                                    }
                                    break;
                                }
                                var errorObj = that.parseJSON(err.response);
                                var errorText = errorObj.error;
                                switch (err.status) {
                                    case 400:
                                        errTip = "请求报文格式错误。";
                                        break;
                                    case 401:
                                        errTip = "客户端认证授权失败。请重试或提交反馈。";
                                        break;
                                    case 405:
                                        errTip = "客户端请求错误。请重试或提交反馈。";
                                        break;
                                    case 579:
                                        errTip = "资源上传成功，但回调失败。";
                                        break;
                                    case 599:
                                        errTip = "网络连接异常。请重试或提交反馈。";
                                        if (!unknow_error_retry(file)) {
                                            return;
                                        }
                                        break;
                                    case 614:
                                        errTip = "文件已存在。";
                                        try {
                                            errorObj = that.parseJSON(errorObj.error);
                                            errorText = errorObj.error || 'file exists';
                                        } catch (e) {
                                            errorText = errorObj.error || 'file exists';
                                        }
                                        break;
                                    case 631:
                                        errTip = "指定空间不存在。";
                                        break;
                                    case 701:
                                        errTip = "上传数据块校验出错。请重试或提交反馈。";
                                        break;
                                    default:
                                        errTip = "未知错误。";
                                        if (!unknow_error_retry(file)) {
                                            return;
                                        }
                                        break;
                                }
                                errTip = errTip + '(' + err.status + '：' + errorText + ')';
                                break;
                            case plupload.SECURITY_ERROR:
                                errTip = '安全配置错误。请联系网站管理员。';
                                break;
                            case plupload.GENERIC_ERROR:
                                errTip = '上传失败。请稍后再试。';
                                break;
                            case plupload.IO_ERROR:
                                errTip = '上传失败。请稍后再试。';
                                break;
                            case plupload.INIT_ERROR:
                                errTip = '网站配置错误。请联系网站管理员。';
                                uploader.destroy();
                                break;
                            default:
                                errTip = err.message + err.details;
                                if (!unknow_error_retry(file)) {
                                    return;
                                }
                                break;
                        }
                        if (_Error_Handler) {
                            _Error_Handler(up, err, errTip);
                        }
                    }
                    up.refresh(); // Reposition Flash/Silverlight
                };
            })(_Error_Handler));
    
            logger.debug("bind Error event");
    
            // bind 'FileUploaded' event
            // intercept the complete of upload
            // - get downtoken from downtoken_url if bucket is private
            // - invoke mkfile api to compose chunks if upload strategy is chunk upload
            uploader.bind('FileUploaded', (function(_FileUploaded_Handler) {
                return function(up, file, info) {
                    logger.debug("FileUploaded event activated");
                    logger.debug("file: ", file);
                    logger.debug("info: ", info);
                    var last_step = function(up, file, info) {
                        if (op.downtoken_url) {
                            // if op.dowontoken_url is not empty
                            // need get downtoken before invoke the _FileUploaded_Handler
                            var ajax_downtoken = that.createAjax();
                            ajax_downtoken.open('POST', op.downtoken_url, true);
                            ajax_downtoken.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
                            ajax_downtoken.onreadystatechange = function() {
                                if (ajax_downtoken.readyState === 4) {
                                    if (ajax_downtoken.status === 200) {
                                        var res_downtoken;
                                        try {
                                            res_downtoken = that.parseJSON(ajax_downtoken.responseText);
                                        } catch (e) {
                                            throw ('invalid json format');
                                        }
                                        var info_extended = {};
                                        plupload.extend(info_extended, that.parseJSON(info), res_downtoken);
                                        if (_FileUploaded_Handler) {
                                            _FileUploaded_Handler(up, file, that.stringifyJSON(info_extended));
                                        }
                                    } else {
                                        uploader.trigger('Error', {
                                            status: ajax_downtoken.status,
                                            response: ajax_downtoken.responseText,
                                            file: file,
                                            code: plupload.HTTP_ERROR
                                        });
                                    }
                                }
                            };
                            ajax_downtoken.send('key=' + that.parseJSON(info).key + '&domain=' + op.domain);
                        } else if (_FileUploaded_Handler) {
                            _FileUploaded_Handler(up, file, info);
                        }
                    };
    
                    var res = that.parseJSON(info.response);
                    ctx = ctx ? ctx : res.ctx;
                    // if ctx is not empty
                    //      that means the upload strategy is chunk upload
                    //      befroe the invoke the last_step
                    //      we need request the mkfile to compose all uploaded chunks
                    // else
                    //      invalke the last_step
                    logger.debug("ctx: ", ctx);
                    if (ctx) {
                        var key = '';
                        logger.debug("save_key: ", op.save_key);
                        if (!op.save_key) {
                            key = getFileKey(up, file, that.key_handler);
                            key = key ? '/key/' + that.URLSafeBase64Encode(key) : '';
                        }
    
                        var fname = '/fname/' + that.URLSafeBase64Encode(file.name);
    
                        logger.debug("op.x_vars: ", op.x_vars);
                        var x_vars = op.x_vars,
                            x_val = '',
                            x_vars_url = '';
                        if (x_vars !== undefined && typeof x_vars === 'object') {
                            for (var x_key in x_vars) {
                                if (x_vars.hasOwnProperty(x_key)) {
                                    if (typeof x_vars[x_key] === 'function') {
                                        x_val = that.URLSafeBase64Encode(x_vars[x_key](up, file));
                                    } else if (typeof x_vars[x_key] !== 'object') {
                                        x_val = that.URLSafeBase64Encode(x_vars[x_key]);
                                    }
                                    x_vars_url += '/x:' + x_key + '/' + x_val;
                                }
                            }
                        }
    
                        var url = qiniuUploadUrl + '/mkfile/' + file.size + key + fname + x_vars_url;
    
                        var ie = that.detectIEVersion();
                        var ajax;
                        if (ie && ie <= 9) {
                            ajax = new mOxie.XMLHttpRequest();
                            mOxie.Env.swf_url = op.flash_swf_url;
                        }else{
                            ajax = that.createAjax();
                        }
                        ajax.open('POST', url, true);
                        ajax.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
                        ajax.setRequestHeader('Authorization', 'UpToken ' + that.token);
                        var onreadystatechange = function(){
                            logger.debug("ajax.readyState: ", ajax.readyState);
                            if (ajax.readyState === 4) {
                                localStorage.removeItem(file.name);
                                var info;
                                if (ajax.status === 200) {
                                    info = ajax.responseText;
                                    logger.debug("mkfile is success: ", info);
                                    last_step(up, file, info);
                                } else {
                                    info = {
                                        status: ajax.status,
                                        response: ajax.responseText,
                                        file: file,
                                        code: -200
                                    };
                                    logger.debug("mkfile is error: ", info);
                                    uploader.trigger('Error', info);
                                }
                            }
                        };
                        if (ie && ie <= 9) {
                            ajax.bind('readystatechange', onreadystatechange);
                        }else{
                            ajax.onreadystatechange = onreadystatechange;
                        }
                        ajax.send(ctx);
                        logger.debug("mkfile: ", url);
                    } else {
                        last_step(up, file, info.response);
                    }
    
                };
            })(_FileUploaded_Handler));
    
            logger.debug("bind FileUploaded event");
    
            // init uploader
            uploader.init();
    
            logger.debug("invoke uploader.init()");
    
            logger.debug("init uploader end");
    
            return uploader;
        };
    
        /**
         * get url by key
         * @param  {String} key of file
         * @return {String} url of file
         */
        this.getUrl = function(key) {
            if (!key) {
                return false;
            }
            key = encodeURI(key);
            var domain = this.domain;
            if (domain.slice(domain.length - 1) !== '/') {
                domain = domain + '/';
            }
            return domain + key;
        };
    
        /**
         * invoke the imageView2 api of Qiniu
         * @param  {Object} api params
         * @param  {String} key of file
         * @return {String} url of processed image
         */
        this.imageView2 = function(op, key) {
            var mode = op.mode || '',
                w = op.w || '',
                h = op.h || '',
                q = op.q || '',
                format = op.format || '';
            if (!mode) {
                return false;
            }
            if (!w && !h) {
                return false;
            }
    
            var imageUrl = 'imageView2/' + mode;
            imageUrl += w ? '/w/' + w : '';
            imageUrl += h ? '/h/' + h : '';
            imageUrl += q ? '/q/' + q : '';
            imageUrl += format ? '/format/' + format : '';
            if (key) {
                imageUrl = this.getUrl(key) + '?' + imageUrl;
            }
            return imageUrl;
        };
    
        /**
         * invoke the imageMogr2 api of Qiniu
         * @param  {Object} api params
         * @param  {String} key of file
         * @return {String} url of processed image
         */
        this.imageMogr2 = function(op, key) {
            var auto_orient = op['auto-orient'] || '',
                thumbnail = op.thumbnail || '',
                strip = op.strip || '',
                gravity = op.gravity || '',
                crop = op.crop || '',
                quality = op.quality || '',
                rotate = op.rotate || '',
                format = op.format || '',
                blur = op.blur || '';
            //Todo check option
    
            var imageUrl = 'imageMogr2';
    
            imageUrl += auto_orient ? '/auto-orient' : '';
            imageUrl += thumbnail ? '/thumbnail/' + thumbnail : '';
            imageUrl += strip ? '/strip' : '';
            imageUrl += gravity ? '/gravity/' + gravity : '';
            imageUrl += quality ? '/quality/' + quality : '';
            imageUrl += crop ? '/crop/' + crop : '';
            imageUrl += rotate ? '/rotate/' + rotate : '';
            imageUrl += format ? '/format/' + format : '';
            imageUrl += blur ? '/blur/' + blur : '';
    
            if (key) {
                imageUrl = this.getUrl(key) + '?' + imageUrl;
            }
            return imageUrl;
        };
    
        /**
         * invoke the watermark api of Qiniu
         * @param  {Object} api params
         * @param  {String} key of file
         * @return {String} url of processed image
         */
        this.watermark = function(op, key) {
            var mode = op.mode;
            if (!mode) {
                return false;
            }
    
            var imageUrl = 'watermark/' + mode;
    
            if (mode === 1) {
                var image = op.image || '';
                if (!image) {
                    return false;
                }
                imageUrl += image ? '/image/' + this.URLSafeBase64Encode(image) : '';
            } else if (mode === 2) {
                var text = op.text ? op.text : '',
                    font = op.font ? op.font : '',
                    fontsize = op.fontsize ? op.fontsize : '',
                    fill = op.fill ? op.fill : '';
                if (!text) {
                    return false;
                }
                imageUrl += text ? '/text/' + this.URLSafeBase64Encode(text) : '';
                imageUrl += font ? '/font/' + this.URLSafeBase64Encode(font) : '';
                imageUrl += fontsize ? '/fontsize/' + fontsize : '';
                imageUrl += fill ? '/fill/' + this.URLSafeBase64Encode(fill) : '';
            } else {
                // Todo mode3
                return false;
            }
    
            var dissolve = op.dissolve || '',
                gravity = op.gravity || '',
                dx = op.dx || '',
                dy = op.dy || '';
    
            imageUrl += dissolve ? '/dissolve/' + dissolve : '';
            imageUrl += gravity ? '/gravity/' + gravity : '';
            imageUrl += dx ? '/dx/' + dx : '';
            imageUrl += dy ? '/dy/' + dy : '';
    
            if (key) {
                imageUrl = this.getUrl(key) + '?' + imageUrl;
            }
            return imageUrl;
        };
    
        /**
         * invoke the imageInfo api of Qiniu
         * @param  {String} key of file
         * @return {Object} image info
         */
        this.imageInfo = function(key) {
            if (!key) {
                return false;
            }
            var url = this.getUrl(key) + '?imageInfo';
            var xhr = this.createAjax();
            var info;
            var that = this;
            xhr.open('GET', url, false);
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    info = that.parseJSON(xhr.responseText);
                }
            };
            xhr.send();
            return info;
        };
    
        /**
         * invoke the exif api of Qiniu
         * @param  {String} key of file
         * @return {Object} image exif
         */
        this.exif = function(key) {
            if (!key) {
                return false;
            }
            var url = this.getUrl(key) + '?exif';
            var xhr = this.createAjax();
            var info;
            var that = this;
            xhr.open('GET', url, false);
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    info = that.parseJSON(xhr.responseText);
                }
            };
            xhr.send();
            return info;
        };
    
        /**
         * invoke the exif or imageInfo api of Qiniu
         * according with type param
         * @param  {String} ['exif'|'imageInfo']type of info
         * @param  {String} key of file
         * @return {Object} image exif or info
         */
        this.get = function(type, key) {
            if (!key || !type) {
                return false;
            }
            if (type === 'exif') {
                return this.exif(key);
            } else if (type === 'imageInfo') {
                return this.imageInfo(key);
            }
            return false;
        };
    
        /**
         * invoke api of Qiniu like a pipeline
         * @param  {Array of Object} params of a series api call
         * each object in array is options of api which name is set as 'fop' property
         * each api's output will be next api's input
         * @param  {String} key of file
         * @return {String|Boolean} url of processed image
         */
        this.pipeline = function(arr, key) {
            var isArray = Object.prototype.toString.call(arr) === '[object Array]';
            var option, errOp, imageUrl = '';
            if (isArray) {
                for (var i = 0, len = arr.length; i < len; i++) {
                    option = arr[i];
                    if (!option.fop) {
                        return false;
                    }
                    switch (option.fop) {
                        case 'watermark':
                            imageUrl += this.watermark(option) + '|';
                            break;
                        case 'imageView2':
                            imageUrl += this.imageView2(option) + '|';
                            break;
                        case 'imageMogr2':
                            imageUrl += this.imageMogr2(option) + '|';
                            break;
                        default:
                            errOp = true;
                            break;
                    }
                    if (errOp) {
                        return false;
                    }
                }
                if (key) {
                    imageUrl = this.getUrl(key) + '?' + imageUrl;
                    var length = imageUrl.length;
                    if (imageUrl.slice(length - 1) === '|') {
                        imageUrl = imageUrl.slice(0, length - 1);
                    }
                }
                return imageUrl;
            }
            return false;
        };
    }
    
    var Qiniu = new QiniuJsSDK();
    
    global.Qiniu = Qiniu;
    
    global.QiniuJsSDK = QiniuJsSDK;
    
    })( window );
    